【LINEミニアプリ】アプリ内の特定の属性を持つ未会員ユーザーを識別する方法を考えてみた

【LINEミニアプリ】アプリ内の特定の属性を持つ未会員ユーザーを識別する方法を考えてみた

Clock Icon2024.10.22

リテールアプリ共創部のるおんです。
LIFFアプリの開発において、アプリ内で特定の属性を持つ会員登録したユーザーと未会員ユーザーを識別する必要性に直面しました。
これまでは会員登録したユーザーのLINE UIDを取得し、そのユーザーの属性に応じてLINE OAMでオーディエンスを作成し、会員向けにメッセージ配信を行なっていました。
しかし、これだと会員登録をしてくれなかったユーザーに対してメッセージ配信などのアクションを実行することができませんでした。また、特定の属性を持つ未会員ユーザーにのみメッセージを配信したかったため、単純に全友達の中から会員を除くユーザーにメッセージ配信をするだけではこの要求に応えられません。
今回はこの課題に対する解決策を考案し実装してみましたので、その方法をご紹介します。

やりたいこと

特定の属性を持つ未会員ユーザーにのみメッセージを配信したい。

我々が開発しているLINEミニアプリでは、アプリの初期アクセス画面においてクエリパラメーターで流入経路を取得するようにしていました。
例)
カフェA: https://miniapp.line.me/xxxxxxxxxxx/cafe=A
カフェB: https://miniapp.line.me/xxxxxxxxxxx/cafe=B

カフェAからLINEミニアプリを追加してくれた未会員ユーザーと、カフェBからLINEミニアプリを追加してくれた未会員ユーザーに対してそれぞれメッセージを出し分けることが今回のゴールです。

これらの課題を解決するために、以下のアプローチを採用しました。
会員テーブルとは別に、一度でもアクセスしたユーザー全員を保存する専用テーブル(Guestテーブル)を作成し、そのテーブルにLINE UIDや名前、流入経路 の情報を格納するのに加えて、会員か未会員かの ステータス を持たせるようにしました。
イメージとしては、以下の図のようなものです。

スクリーンショット 2024-10-24 8.36.42

このようにすることで、Guestテーブルの流入経路ごとに、isMemberステータスがfalseのユーザー群のLINE UIDを使用することで、未会員ユーザーのみに特定アクション(メッセージ一斉配信など)をすることができます。
例えば、以下のように カフェAの未会員ユーザー に対してメッセージを配信したい場合は、1列目と5列目のTAROさんとJIROさんのUIDを使ってオーディンエンスを作成すれば該当ユーザーに対してのみ一斉にメッセージを配信できます。

uid name cafe isMember
U111111 TARO カフェA false
U222222 MARIO カフェB true
U333333 HANAKO カフェB false
U444444 HANAO カフェA true
U55555 JIRO カフェA false

実際にやってみた

ではこの構成を実際に実装してみました。フロントエンドにReact、バックエンドにAWS LambdaをNode.jsを使って実装します。データベースはAmazon DynamoDBを用いています。

以下は簡単な手順です。

  1. 専用テーブル作成
    すべてのLIFFアプリアクセスユーザーの情報を保存するテーブル(Guestテーブル)を新設します。
  2. 初回アクセス時の情報送信(フロントエンド)
    アプリの初期表示画面にて、クエリパラーメーターから 流入経路の情報 ととユーザー情報を取得するための アクセストークン をバックエンドに送信します。
  3. ユーザー情報の保存(バックエンド)
    受け取った流入経路とアクセストークンからユーザー情報を取得し、Guestテーブルに保存する。
    会員登録をする際にisMemberステータスを更新。

インフラストラクチャはAWS CDKを用いてサクッと作ってみました。

CDK

linUserIdをパーティションキーとするDynamoDBのGuestテーブルと、保存処理をするためのLambda、エンドポイントとしてのAPI Gatewayを作成しています。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as aws_dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { RemovalPolicy } from 'aws-cdk-lib';

export class GuestLiffTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    /**
     * GuestUserを登録するLambda
     */
    const registerGuestUserFn = new nodejs.NodejsFunction(this, "registerGuestUserFn", {
      entry: "server/handler/registerGuestUserHandler.ts",
      runtime: lambda.Runtime.NODEJS_20_X,
      functionName: "registerGuestUserFn",
      description: "アクセスしたユーザーをすべて保存するLambda関数",
      architecture: lambda.Architecture.ARM_64,
    });

    /**
     * ゲスト用のAPI Gateway
     */
    const api = new apigateway.LambdaRestApi(this, "registerGuestUserApi", {
      handler: registerGuestUserFn,
      proxy: false,
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: apigateway.Cors.DEFAULT_HEADERS,
        statusCode: 200,
      },

    });

    const registerGuestUserIntegration = new apigateway.LambdaIntegration(registerGuestUserFn);
    api.root.addMethod("POST", registerGuestUserIntegration)

    /**
     * ゲスト用のDynamoDB
     */
    const guestTable = new aws_dynamodb.Table(
      this,
      "guestTable",
      {
        partitionKey: {
          name: "lineUserId",
          type: aws_dynamodb.AttributeType.STRING,
        },
        billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
        removalPolicy: RemovalPolicy.RETAIN,
        tableName: "Guest",
        pointInTimeRecovery: true,
      },
    );

    guestTable.grantReadWriteData(registerGuestUserFn);
  }
}

フロントエンド

ユーザーが最初にアクセスするのが以下のような画面だとします。

IMG_0902

この画面を開いた瞬間、useEffectを使用してアクセストークンをバックエンドに送信し、ゲストユーザーを保存するエンドポイントを叩きます。

App.tsx
import { useEffect, useState } from "react";
import liff from "@line/liff";
import "./App.css";
import axios from "axios";

function App() {
  // 省略
  const registerGuestUser = async () => {
    // クエリパラメーターからcafeIdを取得
    const cafeId = new URLSearchParams(window.location.search).get("cafeId");
    // LINEユーザーのアクセストークンを取得
    const accessToken = await liff.getAccessToken();

    if (cafeId && accessToken) {
      await axios.post(
        "https://ihyybtxzsc.execute-api.ap-northeast-1.amazonaws.com/prod", // サーバーサイドへのリクエストエンドポイント
        { accessToken, cafeId }
      );
    }
  };

+ // 初回アクセス時に実行
+  useEffect(() => {
+   registerGuestUser();
+ }, []);

  return (
    <div className="App">
      <h1>アプリへようこそ!</h1>
      <a href="https://xxxxxxxx/">
        会員登録はこちらから
      </a>
    </div>
  );
}

export default App;

フロント側でユーザー情報を取得してバックエンドに送信するのではなく、必ずアクセストークンやIDトークンなどを送信するようにしてください。詳しくはこちらの記事で解説しています。
https://dev.classmethod.jp/articles/liff-line-user-profile-in-server-side/

バックエンド

次に、バックエンドを実装します。フロントから送られてきたアクセストークンを使用してユーザー情報を取得し、流入経路とisMemberステータスをfalseとしてデータベースに保存します。

以下はLambda関数の例です。

import axios from "axios";
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb';

// DynamoDBのクライアントの初期化
const dynamoDB = new DynamoDBClient({ region: 'ap-northeast-1' });
const dynamoDBDocumentClient = DynamoDBDocumentClient.from(dynamoDB);

export const handler = async (event: any) => {
  const { accessToken, cafeId } = JSON.parse(event.body);

  // LINEユーザー情報を取得
  const userInfoResponse = await axios.get("https://api.line.me/v2/profile", {
    headers: {
      Authorization: `Bearer ${accessToken}`
    }
  });

  const lineUserId = userInfoResponse.data.userId;

  // DynamoDBからユーザーを取得
  const guestUser = await dynamoDBDocumentClient.send(new GetCommand({
    TableName: 'Guest',
    Key: {
      lineUserId: lineUserId
    }
  }));

  if (!guestUser.Item) {
    // ゲストが存在しない場合、新規作成  
+   await dynamoDBDocumentClient.send(new PutCommand({
+     TableName: 'Guest',
+     Item: {
+       lineUserId: lineUserId,
+       lineDisplayName: userInfoResponse.data.displayName,
+       cafeId: cafeId,
+       isMember: false // ステータスをfalseにして、未会員ユーザーとして識別できるようにする
+     }
    }));

    return {
      statusCode: 201,
      body: JSON.stringify({ message: "新しいユーザー情報を保存しました" })
    };
  } else {
    // ユーザーが既に存在する場合
    return {
      statusCode: 200,
      body: JSON.stringify({ message: "ユーザーは既に登録されています" })
    };
  }
};

実際にこのアプリにアクセスしてみると、ユーザー情報が保存されていることが確認できます。
スクリーンショット 2024-10-24 9.00.55
これで未会員のユーザー情報を取得できるようになりました。

では、次に会員登録をした際にはこのisMemberステータスをtrueにして、会員登録済みかどうかをわかるようにします。今回は実装していませんが、すでに会員登録処理がある前提で進めます。

// 会員情報の登録の際に、Guestテーブルの該当ユーザーのisMemberステータスを更新する
  await dynamoDBDocumentClient.send(new UpdateCommand({
    TableName: 'Guest',
    Key: {
      lineUserId: userInfoResponse.data.userId
    },
    UpdateExpression: 'SET isMember = :isMember',
    ExpressionAttributeValues: {
+     ':isMember': true
    },
    ReturnValues: 'UPDATED_NEW'
  }));

  // 会員情報を登録する処理
  // 省略

  return {
    statusCode: 200,
    body: JSON.stringify({ message: "会員情報を保存しました" })
  };
};

会員登録をすると同時に、GuestテーブルのisMemberステータスがtrueになっていることが確認できます。
スクリーンショット 2024-10-24 9.02.11

このようにすることで、利用ユーザー数が増えた場合に、ユーザーの属性と、未会員ユーザーなのか会員登録済みユーザーなのかを識別することができますね。

例えば、カフェAかつ、未会員のユーザーは以下の赤線内の二人であることがわかります。

スクリーンショット 2024-10-25 4.45.21

そして、この2人のLINE UIDを用いてオーディエンスを作成し、メッセージを配信するなどのアクションをすることが可能になります。

おわりに

今回、LIFFアプリにおける特定の属性の会員・未会員ユーザーの識別方法について、具体的な実装例を交えて紹介しました。この手法を用いることで、特定の流入経路や、ユーザーの属性を持つ未会員ユーザーに特化したメッセージ配信や施策を実施できるようになり、会員登録率の向上につながる可能性があります。

以上、どなたかの参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.